package com.ht.modules.system.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.toolkit.*;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.ResultHandler;

import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Statement;
import java.util.*;
import java.util.concurrent.TimeUnit;

@Intercepts({@Signature(
        type = StatementHandler.class,
        method = "query",
        args = {Statement.class, ResultHandler.class}
), @Signature(
        type = StatementHandler.class,
        method = "update",
        args = {Statement.class}
), @Signature(
        type = StatementHandler.class,
        method = "batch",
        args = {Statement.class}
)})
public class PerformanceInterceptor implements Interceptor {
    private static final Log logger = LogFactory.getLog(PerformanceInterceptor.class);
    private static final String DruidPooledPreparedStatement = "com.alibaba.druid.pool.DruidPooledPreparedStatement";
    private static final String T4CPreparedStatement = "oracle.jdbc.driver.T4CPreparedStatement";
    private static final String OraclePreparedStatementWrapper = "oracle.jdbc.driver.OraclePreparedStatementWrapper";
    private long maxTime = 0L;
    private boolean format = false;
    private boolean writeInLog = false;
    private Method oracleGetOriginalSqlMethod;
    private Method druidGetSQLMethod;

    public PerformanceInterceptor() {
    }

    public Object intercept(Invocation invocation) throws Throwable {
        Object firstArg = invocation.getArgs()[0];
        Statement statement;
        if (Proxy.isProxyClass(firstArg.getClass())) {
            statement = (Statement) SystemMetaObject.forObject(firstArg).getValue("h.statement");
        } else {
            statement = (Statement) firstArg;
        }
        MetaObject stmtMetaObj = SystemMetaObject.forObject(statement);

        try {
            statement = (Statement) stmtMetaObj.getValue("stmt.statement");
        } catch (Exception var20) {
        }

        if (stmtMetaObj.hasGetter("delegate")) {
            try {
                statement = (Statement) stmtMetaObj.getValue("delegate");
            } catch (Exception var19) {
            }
        }

        String originalSql = null;
        String stmtClassName = statement.getClass().getName();
        Class clazz;
        Object stmtSql;
        if ("com.alibaba.druid.pool.DruidPooledPreparedStatement".equals(stmtClassName)) {
            try {
                if (this.druidGetSQLMethod == null) {
                    clazz = Class.forName("com.alibaba.druid.pool.DruidPooledPreparedStatement");
                    this.druidGetSQLMethod = clazz.getMethod("getSql");
                }
                stmtSql = this.druidGetSQLMethod.invoke(statement);
                if (stmtSql instanceof String) {
                    originalSql = (String) stmtSql;
                }
            } catch (Exception var18) {
                var18.printStackTrace();
            }
        } else if ("oracle.jdbc.driver.T4CPreparedStatement".equals(stmtClassName) || "oracle.jdbc.driver.OraclePreparedStatementWrapper".equals(stmtClassName)) {
            try {
                if (this.oracleGetOriginalSqlMethod != null) {
                    stmtSql = this.oracleGetOriginalSqlMethod.invoke(statement);
                    if (stmtSql instanceof String) {
                        originalSql = (String) stmtSql;
                    }
                } else {
                    clazz = Class.forName(stmtClassName);
                    this.oracleGetOriginalSqlMethod = this.getMethodRegular(clazz, "getOriginalSql");
                    if (this.oracleGetOriginalSqlMethod != null) {
                        this.oracleGetOriginalSqlMethod.setAccessible(true);
                        if (null != this.oracleGetOriginalSqlMethod) {
                            Object invoke = this.oracleGetOriginalSqlMethod.invoke(statement);
                            if (invoke instanceof String) {
                                originalSql = (String) invoke;
                            }
                        }
                    }
                }
            } catch (Exception var17) {
            }
        } else if ("dm.jdbc.driver.DmdbPreparedStatement".equals(stmtClassName)) {
//            try {
//                if (statement instanceof DmdbStatement) {
//                    DmdbStatement dmdbStatement = (DmdbStatement) statement;
//                    originalSql = dmdbStatement.nativeSql;
//                }
//            } catch (Exception var18) {
//                var18.printStackTrace();
//            }
        }

        if (originalSql == null) {
            originalSql = statement.toString();
        }
        originalSql = originalSql.replaceAll("[\\s]+", " ");
        int index = this.indexOfSqlStart(originalSql);
        if (index > 0) {
            originalSql = originalSql.substring(index);
        }
        long start = System.currentTimeMillis();
        Object result = invocation.proceed();
        BigDecimal timing = new BigDecimal(System.currentTimeMillis() - start);
        Object target = PluginUtils.realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(target);
        MappedStatement ms = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        StringBuilder formatSql = (new StringBuilder()).append(" Time：").append(timing).append(" ms - ID：").append(ms.getId()).append("\n").append(" EXPLAIN\n ").append(SqlUtils.sqlFormat(originalSql, this.format)).append("\n");
        if (this.isWriteInLog()) {
            if (this.getMaxTime() >= 1L && timing.compareTo(new BigDecimal(this.getMaxTime())) > 0) {
                logger.error(formatSql.toString());
            } else {
                logger.debug(formatSql.toString());
            }
        } else {
            System.err.println(formatSql);
            Assert.isFalse(this.getMaxTime() >= 1L && timing.compareTo(new BigDecimal(this.getMaxTime())) > 0, " The SQL execution time is too large, please optimize ! ");
        }
        return result;
    }

    public Object plugin(Object target) {
        return target instanceof StatementHandler ? Plugin.wrap(target, this) : target;
    }

    public void setProperties(Properties prop) {
        String maxTime = prop.getProperty("maxTime");
        String format = prop.getProperty("format");
        if (StringUtils.isNotEmpty(maxTime)) {
            this.maxTime = Long.parseLong(maxTime);
        }

        if (StringUtils.isNotEmpty(format)) {
            this.format = Boolean.valueOf(format);
        }

    }

    public Method getMethodRegular(Class<?> clazz, String methodName) {
        if (Object.class.equals(clazz)) {
            return null;
        } else {
            Method[] var3 = clazz.getDeclaredMethods();
            int var4 = var3.length;
            for (Method method : var3) {
                if (method.getName().equals(methodName)) {
                    return method;
                }
            }

            return this.getMethodRegular(clazz.getSuperclass(), methodName);
        }
    }

    private int indexOfSqlStart(String sql) {
        String upperCaseSql = sql.toUpperCase();
        Set<Integer> set = new HashSet<>();
        set.add(upperCaseSql.indexOf("SELECT "));
        set.add(upperCaseSql.indexOf("UPDATE "));
        set.add(upperCaseSql.indexOf("INSERT "));
        set.add(upperCaseSql.indexOf("DELETE "));
        set.remove(-1);
        if (CollectionUtils.isEmpty(set)) {
            return -1;
        } else {
            List<Integer> list = new ArrayList<>(set);
            list.sort(Comparator.naturalOrder());
            return (Integer) list.get(0);
        }
    }

    public PerformanceInterceptor setMaxTime(long maxTime) {
        this.maxTime = maxTime;
        return this;
    }

    public long getMaxTime() {
        return this.maxTime;
    }

    public PerformanceInterceptor setFormat(boolean format) {
        this.format = format;
        return this;
    }

    public boolean isFormat() {
        return this.format;
    }

    public PerformanceInterceptor setWriteInLog(boolean writeInLog) {
        this.writeInLog = writeInLog;
        return this;
    }

    public boolean isWriteInLog() {
        return this.writeInLog;
    }

    public static class SqlFormatter {
        public static final String WHITESPACE = " \n\r\f\t";
        private static final Set<String> BEGIN_CLAUSES = new HashSet<>();
        private static final Set<String> END_CLAUSES = new HashSet<>();
        private static final Set<String> LOGICAL = new HashSet<>();
        private static final Set<String> QUANTIFIERS = new HashSet<>();
        private static final Set<String> DML = new HashSet<>();
        private static final Set<String> MISC = new HashSet<>();
        private static final String INDENT_STRING = "    ";
        private static final String INITIAL = "\n    ";

        public SqlFormatter() {
        }

        public String format(String source) {
            return (new FormatProcess(source)).perform();
        }

        static {
            BEGIN_CLAUSES.add("left");
            BEGIN_CLAUSES.add("right");
            BEGIN_CLAUSES.add("inner");
            BEGIN_CLAUSES.add("outer");
            BEGIN_CLAUSES.add("group");
            BEGIN_CLAUSES.add("order");
            END_CLAUSES.add("where");
            END_CLAUSES.add("set");
            END_CLAUSES.add("having");
            END_CLAUSES.add("join");
            END_CLAUSES.add("from");
            END_CLAUSES.add("by");
            END_CLAUSES.add("into");
            END_CLAUSES.add("union");
            LOGICAL.add("and");
            LOGICAL.add("or");
            LOGICAL.add("when");
            LOGICAL.add("else");
            LOGICAL.add("end");
            QUANTIFIERS.add("in");
            QUANTIFIERS.add("all");
            QUANTIFIERS.add("exists");
            QUANTIFIERS.add("some");
            QUANTIFIERS.add("any");
            DML.add("insert");
            DML.add("update");
            DML.add("delete");
            MISC.add("select");
            MISC.add("on");
        }

        private static class FormatProcess {
            boolean beginLine = true;
            boolean afterBeginBeforeEnd;
            boolean afterByOrSetOrFromOrSelect;
            boolean afterValues;
            boolean afterOn;
            boolean afterBetween;
            boolean afterInsert;
            int inFunction;
            int parensSinceSelect;
            private final LinkedList<Integer> parenCounts = new LinkedList();
            private final LinkedList<Boolean> afterByOrFromOrSelects = new LinkedList();
            int indent = 1;
            final StringBuilder result = new StringBuilder();
            final StringTokenizer tokens;
            String lastToken;
            String token;
            String lcToken;

            public FormatProcess(String sql) {
                this.tokens = new StringTokenizer(sql, "()+*/-=<>'`\"[], \n\r\f\t", true);
            }

            public String perform() {
                this.result.append("\n    ");

                while (this.tokens.hasMoreTokens()) {
                    this.token = this.tokens.nextToken();
                    this.lcToken = this.token.toLowerCase(Locale.ROOT);
                    String t;
                    if ("'".equals(this.token)) {
                        t = "";

                        do {
                            try {
                                t = this.tokens.nextToken();
                            } catch (Exception var3) {
                            }

                            this.token = this.token + t;
                        } while (!"'".equals(t) && this.tokens.hasMoreTokens());
                    } else if ("\"".equals(this.token)) {
                        do {
                            t = this.tokens.nextToken();
                            this.token = this.token + t;
                        } while (!"\"".equals(t));
                    }

                    if (this.afterByOrSetOrFromOrSelect && ",".equals(this.token)) {
                        this.commaAfterByOrFromOrSelect();
                    } else if (this.afterOn && ",".equals(this.token)) {
                        this.commaAfterOn();
                    } else if ("(".equals(this.token)) {
                        this.openParen();
                    } else if (")".equals(this.token)) {
                        this.closeParen();
                    } else if (SqlFormatter.BEGIN_CLAUSES.contains(this.lcToken)) {
                        this.beginNewClause();
                    } else if (SqlFormatter.END_CLAUSES.contains(this.lcToken)) {
                        this.endNewClause();
                    } else if ("select".equals(this.lcToken)) {
                        this.select();
                    } else if (SqlFormatter.DML.contains(this.lcToken)) {
                        this.updateOrInsertOrDelete();
                    } else if ("values".equals(this.lcToken)) {
                        this.values();
                    } else if ("on".equals(this.lcToken)) {
                        this.on();
                    } else if (this.afterBetween && "and".equals(this.lcToken)) {
                        this.misc();
                        this.afterBetween = false;
                    } else if (SqlFormatter.LOGICAL.contains(this.lcToken)) {
                        this.logical();
                    } else if (isWhitespace(this.token)) {
                        this.white();
                    } else {
                        this.misc();
                    }

                    if (!isWhitespace(this.token)) {
                        this.lastToken = this.lcToken;
                    }
                }

                return this.result.toString();
            }

            private void commaAfterOn() {
                this.out();
                --this.indent;
                this.newline();
                this.afterOn = false;
                this.afterByOrSetOrFromOrSelect = true;
            }

            private void commaAfterByOrFromOrSelect() {
                this.out();
                this.newline();
            }

            private void logical() {
                if ("end".equals(this.lcToken)) {
                    --this.indent;
                }

                this.newline();
                this.out();
                this.beginLine = false;
            }

            private void on() {
                ++this.indent;
                this.afterOn = true;
                this.newline();
                this.out();
                this.beginLine = false;
            }

            private void misc() {
                this.out();
                if ("between".equals(this.lcToken)) {
                    this.afterBetween = true;
                }

                if (this.afterInsert) {
                    this.newline();
                    this.afterInsert = false;
                } else {
                    this.beginLine = false;
                    if ("case".equals(this.lcToken)) {
                        ++this.indent;
                    }
                }

            }

            private void white() {
                if (!this.beginLine) {
                    this.result.append(" ");
                }

            }

            private void updateOrInsertOrDelete() {
                this.out();
                ++this.indent;
                this.beginLine = false;
                if ("update".equals(this.lcToken)) {
                    this.newline();
                }

                if ("insert".equals(this.lcToken)) {
                    this.afterInsert = true;
                }

            }

            private void select() {
                this.out();
                ++this.indent;
                this.newline();
                this.parenCounts.addLast(this.parensSinceSelect);
                this.afterByOrFromOrSelects.addLast(this.afterByOrSetOrFromOrSelect);
                this.parensSinceSelect = 0;
                this.afterByOrSetOrFromOrSelect = true;
            }

            private void out() {
                this.result.append(this.token);
            }

            private void endNewClause() {
                if (!this.afterBeginBeforeEnd) {
                    --this.indent;
                    if (this.afterOn) {
                        --this.indent;
                        this.afterOn = false;
                    }

                    this.newline();
                }

                this.out();
                if (!"union".equals(this.lcToken)) {
                    ++this.indent;
                }

                this.newline();
                this.afterBeginBeforeEnd = false;
                this.afterByOrSetOrFromOrSelect = "by".equals(this.lcToken) || "set".equals(this.lcToken) || "from".equals(this.lcToken);
            }

            private void beginNewClause() {
                if (!this.afterBeginBeforeEnd) {
                    if (this.afterOn) {
                        --this.indent;
                        this.afterOn = false;
                    }

                    --this.indent;
                    this.newline();
                }

                this.out();
                this.beginLine = false;
                this.afterBeginBeforeEnd = true;
            }

            private void values() {
                --this.indent;
                this.newline();
                this.out();
                ++this.indent;
                this.newline();
                this.afterValues = true;
            }

            private void closeParen() {
                --this.parensSinceSelect;
                if (this.parensSinceSelect < 0) {
                    --this.indent;
                    this.parensSinceSelect = (Integer) this.parenCounts.removeLast();
                    this.afterByOrSetOrFromOrSelect = (Boolean) this.afterByOrFromOrSelects.removeLast();
                }

                if (this.inFunction > 0) {
                    --this.inFunction;
                    this.out();
                } else {
                    if (!this.afterByOrSetOrFromOrSelect) {
                        --this.indent;
                        this.newline();
                    }

                    this.out();
                }

                this.beginLine = false;
            }

            private void openParen() {
                if (isFunctionName(this.lastToken) || this.inFunction > 0) {
                    ++this.inFunction;
                }

                this.beginLine = false;
                if (this.inFunction > 0) {
                    this.out();
                } else {
                    this.out();
                    if (!this.afterByOrSetOrFromOrSelect) {
                        ++this.indent;
                        this.newline();
                        this.beginLine = true;
                    }
                }

                ++this.parensSinceSelect;
            }

            private static boolean isFunctionName(String tok) {
                char begin = tok.charAt(0);
                boolean isIdentifier = Character.isJavaIdentifierStart(begin) || '"' == begin;
                return isIdentifier && !SqlFormatter.LOGICAL.contains(tok) && !SqlFormatter.END_CLAUSES.contains(tok) && !SqlFormatter.QUANTIFIERS.contains(tok) && !SqlFormatter.DML.contains(tok) && !SqlFormatter.MISC.contains(tok);
            }

            private static boolean isWhitespace(String token) {
                return " \n\r\f\t".contains(token);
            }

            private void newline() {
                this.result.append("\n");

                for (int i = 0; i < this.indent; ++i) {
                    this.result.append("    ");
                }

                this.beginLine = true;
            }
        }
    }

    public enum SqlLike {
        LEFT("left", "左边%"),
        RIGHT("right", "右边%"),
        CUSTOM("custom", "定制"),
        DEFAULT("default", "两边%");

        private final String type;
        private final String desc;

        private SqlLike(String type, String desc) {
            this.type = type;
            this.desc = desc;
        }

        public String getType() {
            return this.type;
        }

        public String getDesc() {
            return this.desc;
        }
    }

    public static class SqlUtils {
        private static final SqlFormatter SQL_FORMATTER = new SqlFormatter();

        public SqlUtils() {
        }

        public static String sqlFormat(String boundSql, boolean format) {
            if (format) {
                try {
                    return SQL_FORMATTER.format(boundSql);
                } catch (Exception var3) {
                }
            }

            return boundSql;
        }

        public static String concatLike(String str, SqlLike type) {
            StringBuilder builder = new StringBuilder(str.length() + 3);
            switch (type) {
                case LEFT:
                    builder.append("%").append(str);
                    break;
                case RIGHT:
                    builder.append(str).append("%");
                    break;
                case CUSTOM:
                    builder.append(str);
                    break;
                default:
                    builder.append("%").append(str).append("%");
            }

            return builder.toString();
        }

        public static String sqlWordConvert(DbType dbType, String val, boolean isColumn) {
            if (dbType != DbType.POSTGRE_SQL) {
                return val;
            } else {
                return !isColumn || !StringUtils.isNotColumnName(val) && !val.toLowerCase().equals(val) ? String.format("\"%s\"", val) : val;
            }
        }

        public static String stripSqlInjection(String sql) {
            Assert.notNull(sql, "strip sql is null.", new Object[0]);
            return sql.replaceAll("('.+--)|(--)|(\\|)|(%7C)", "");
        }
    }
}
